Skip to content

Add Clash.Class.NumConvert #2915

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Jun 5, 2025
Merged

Conversation

martijnbastiaan
Copy link
Member

@martijnbastiaan martijnbastiaan commented Mar 30, 2025

Utilities for safely converting between various Clash number types.

To discuss / investigate (but maybe not for this PR?)

  • The instances add 1 <= n constraints to every result where the result is Index n
    • This makes sense from a perspective of: Index 0 has no inhabitants, so we cannot create it. On the other hand, we can of course just call errorX "foo". I'm not sure whether I would say this violates the "guaranteed to succeed" rule.
  • The instances add 1 <= n constraints to every argument where the argument is Index n
    • This is similar to the first point, but differs in that we shove the responsibility for creating these values to the caller.
  • convert and maybeConvert will always return XException if given an XException.
    • Does this make sense for converting BitVector 0 to Index/Unsigned/Signed? The HDL will happily do it, why wouldn't Clash simulation? Similarly, any BitVector n could be repacked to be partially undefined if converted to another BitVector m where m > n.

I'd push for kicking these questions to after this PR, because:

  • We can always relax constraints (but not introduce them)
  • We can always relax XException behavior (but not introduce it).
  • Having a safe way of converting between Clash types is way more important than dropping a minor constraint.
  • It's a fairly delicate thing I don't think we have a proper/consistent view on yet across clash-prelude.

Still TODO:

  • Write a changelog entry (see changelog/README.md)
  • Check copyright notices are up to date in edited files
  • Decide whether to call it Clash.Class.Convert or Clash.Class.Convertible. The latter has some buy-in (?) from the community, given that covertible already exists. On the other hand, this might make users think it is convertible, but it isn't. NumConvert!

@DigitalBrains1

This comment was marked as resolved.

@DigitalBrains1 DigitalBrains1 self-requested a review March 30, 2025 19:19
@martijnbastiaan

This comment was marked as resolved.

@DigitalBrains1
Copy link
Member

DigitalBrains1 commented Mar 30, 2025

I'd vote against naming it Convertible, since I think GHC will by default not qualify the class name in an error message of the form No instance for ‘Convertible A B’, which might be very misleading.

(I really want to make the silly pun No instance for ‘Convertible Coupé Roadster’ but it seems confusing... X-D)

@martijnbastiaan martijnbastiaan force-pushed the martijn/add-clash-class-convert branch 2 times, most recently from d8f851c to 25c7757 Compare April 1, 2025 20:43
Copy link
Member

@kleinreact kleinreact left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM overall.

I'm going to accept this, because I cannot see anything really controversial in the sense that it would be hard to fix it later on.

Regarding the Convert vs Convertible question: I don't like either name, as they both suggest much freedom in terms of which types could be converted with the classes. Your documentation clearly restricts the setting to number types, which is in contrast to Convertible for example, which is also open for conversion between custom non-number types. So maybe we should keep the users of that package happy by not producing any name clash at this point.

A name like changeNumType would make it more clear to me what the purpose of this function is. Convert, Convertible, and convert are just super generic names!

I still find it strange to consider BitVector a number type. If we are going to have a discussion about this at some point, then I'd suggest to keep it out here for the time being. Adding it later is easy and the user still can define the instance locally, if necessary.

I don't like the fact that we need to have two classes in the end, but I get the point that the need for individual selection of constraints per instance introduces that requirement.

@martijnbastiaan
Copy link
Member Author

Thanks for the comments @kleinreact, I'll get to them, though I'm fairly time constrained at the moment :(.

I think your comment about the name convert makes a lot of sense. Maybe convertNum? You're right that the class really constrains it to just number types. (Maybe that is an issue?)

@DigitalBrains1
Copy link
Member

Somehow I feel NumConvert feels more natural than ConvertNum for the class name... But not so for the function name.

Also, we could rewrite the laws such that the class can also be used for non-numbers. I think that would work?

Copy link
Member

@DigitalBrains1 DigitalBrains1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I also feel Convert is too generic a name and squats too much, except if it is only to be used qualified. I'm personally leaning towards renaming it NumConvert, numConvert, MaybeNumConvert and maybeNumConvert as the names used, and exporting them from Prelude. Note that you currently to not export them from Prelude, I'd like that to be added.

Something I noticed is that for Convert, you seem to put a bang pattern only on the instances that need one to preserve the XException, yet on MaybeConvert you just bang almost everything. Was this intended? I'm not against it, I'm just wondering.

@martijnbastiaan
Copy link
Member Author

@DigitalBrains1 For some reason GitHub doesn't let me reply to your comment on the word total, so I'll reply here: totally fair criticism! I think lossless is the word I want to use. (After rephrasing the sentence a bit.)

@DigitalBrains1
Copy link
Member

DigitalBrains1 commented May 17, 2025

For some reason GitHub doesn't let me reply to your comment on the word total

Yeah, that's a confusing formatting issue. As part of a review, you can reply to an earlier review comment. It then shows it in the original place where you can reply, and also as part of the new review. If you click on the date in the header of the latter (3 weeks ago it currently says), you can go to the former and comment... why they did this, I don't know.

I think lossless is fair, although you could argue that bitCoerce @(Unsigned 8) @(Signed 8) 200 is in fact lossless even though the result is -56. But when put into context, lossless is in fact a proper description that people should be able to understand.

[edit]

totally fair criticism!

Wait, was that a pun? Did I miss a pun on my first read? X-D
Nice one!
[/edit]

@martijnbastiaan
Copy link
Member Author

martijnbastiaan commented May 17, 2025

Me? Puns? I wouldn't dare!

@martijnbastiaan martijnbastiaan force-pushed the martijn/add-clash-class-convert branch 3 times, most recently from 0ba7f07 to 73d2424 Compare May 18, 2025 18:11
@martijnbastiaan martijnbastiaan changed the title Add Clash.Class.Convert Add Clash.Class.NumConvert May 18, 2025
@martijnbastiaan martijnbastiaan force-pushed the martijn/add-clash-class-convert branch 4 times, most recently from 13064f3 to b805b7b Compare May 18, 2025 18:49
@martijnbastiaan martijnbastiaan force-pushed the martijn/add-clash-class-convert branch from b805b7b to 42406b1 Compare May 18, 2025 18:54
@martijnbastiaan
Copy link
Member Author

martijnbastiaan commented May 18, 2025

I've done a couple of things (now hidden in resolved conversations):

  • I've renamed the class
  • I've touched up the documentation pointed out by the reviews
  • I've fixed a few issues with the test bench
  • I've added instances for Bit

I think I got to all the comments. Edit: export from Prelude TBD.

@martijnbastiaan
Copy link
Member Author

Something I noticed is that for Convert, you seem to put a bang pattern only on the instances that need one to preserve the XException, yet on MaybeConvert you just bang almost everything. Was this intended? I'm not against it, I'm just wondering.

I mostly followed the error messages brought up by the tests. TBF, it is a bit too much for me to do precisely so I might have been over eager..

@martijnbastiaan martijnbastiaan force-pushed the martijn/add-clash-class-convert branch 2 times, most recently from 9a88bca to d74051a Compare May 19, 2025 07:25
Utilities for safely converting between various Clash number types
@martijnbastiaan martijnbastiaan force-pushed the martijn/add-clash-class-convert branch from d74051a to 530541f Compare May 19, 2025 07:47
Copy link
Member

@DigitalBrains1 DigitalBrains1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice work on the improvements! It did introduce a few more issues, but I think after that, that should be it. As discussed, I will add the improvements as a commit to save you the trouble. But the commit follows tomorrow, I'm already past out of time right now.

Comment on lines 8 to 10
between different number types (e.g., 'Clash.Sized.Unsigned.Unsigned' to
'Clash.Sized.Signed.Signed') and that it is not always clear how to do so
properly. Two classes are exported:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
between different number types (e.g., 'Clash.Sized.Unsigned.Unsigned' to
'Clash.Sized.Signed.Signed') and that it is not always clear how to do so
properly. Two classes are exported:
between different number types (e.g., @Unsigned@ to @Signed@) and that it is not
always clear how to do so properly. Two classes are exported:

With GHC 8.10, you cannot link to qualified type names :-(. Only terms. If you want to link to a type, you need to import it and use it unqualified in the Haddock.

Comment on lines 23 to 25
1. It offers no partial functions.
2. All its conversions are translatable to synthesizable HDL.
3. It is focused on (Clash's) number types
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stupid little stylistic detail: either all these sentence fragments need to omit the period, or they all should have a period. I do believe I omit the period when I add documentation.

> x == fromMaybe x (maybeNumConvert @a @b x >>= maybeNumConvert @b @a)

for all values @x@ of type @a@. It should also preserve the numerical value
interpretation of the bits. For types that have an "Integral" instance, this
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interpretation of the bits. For types that have an "Integral" instance, this
interpretation of the bits. For types that have an @Integral@ instance, this

Double quotes link to modules, not declarations. I don't think the type is in scope, so let's just use typewriter font.

> isJust (maybeNumConvert @a @b x) `implies` isJust (maybeNumConvert @a @b x >>= maybeNumConvert @b @a)

A conversion should succeed if and only if the value is representable in the
target type. For types that have a "Bounded" and "Integral" instance, this
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
target type. For types that have a "Bounded" and "Integral" instance, this
target type. For types that have a @Bounded@ and @Integral@ instance, this

> Just x == maybeNumConvert (numConvert @a @b x)

for all values @x@ of type @a@. It should also preserve the numerical value
interpretation of the bits. For types that have an "Integral" instance, this
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
interpretation of the bits. For types that have an "Integral" instance, this
interpretation of the bits. For types that have an @Integral@ instance, this


Instances should make sure their constraints are as \"tight\" as possible. I.e.,
if an instance's constraints cannot be satisfied, then for the same types
'Clash.Class.NumConvert.maybeConvert' should return 'Nothing' for one or more
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
'Clash.Class.NumConvert.maybeConvert' should return 'Nothing' for one or more
'Clash.Class.NumConvert.maybeNumConvert' should return 'Nothing' for one or more

Additionally, any implementation should be translatable to synthesizable HDL.
-}
class MaybeNumConvert a b where
{- | NumConvert a supplied value of type @a@ to a value of type @b@. If the value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{- | NumConvert a supplied value of type @a@ to a value of type @b@. If the value
{- | Convert a supplied value of type @a@ to a value of type @b@. If the value

As discussed

Additionally, any implementation should be translatable to synthesizable HDL.
-}
class NumConvert a b where
{- | NumConvert a supplied value of type @a@ to a value of type @b@. The conversion
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
{- | NumConvert a supplied value of type @a@ to a value of type @b@. The conversion
{- | Convert a supplied value of type @a@ to a value of type @b@. The conversion

As discussed

Comment on lines 110 to 112
-- | Checks whether an 'XException' in, means an 'XException' out
convertXException :: forall a b. (MaybeNumConvert a b) => Proxy a -> Proxy b -> Bool
convertXException _ _ = isLeft $ isX $ maybeNumConvert @a @b (errorX "" :: a)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
-- | Checks whether an 'XException' in, means an 'XException' out
convertXException :: forall a b. (MaybeNumConvert a b) => Proxy a -> Proxy b -> Bool
convertXException _ _ = isLeft $ isX $ maybeNumConvert @a @b (errorX "" :: a)
-- | Checks whether an 'XException' in, means an 'XException' out
convertXException :: forall a b. (MaybeNumConvert a b) => Proxy a -> Proxy b -> Bool
convertXException _ _ = case isX $ maybeNumConvert @a @b (errorX "BOO" :: a) of
Left s -> "BOO" `L.isInfixOf` s
Right _ -> False

Needs some adjustments to imports too. But the test passes!

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

errorX "BOO"

👻

(since Haskell supports Unicode, you could use that as the error message)

.
.
.

(please don't use errorX "👻")

Copy link
Member

@DigitalBrains1 DigitalBrains1 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed, I've added a commit implementing my change requests. When you squash the merge, please remove the Co-authored by Peter, a bit of review is not co-authorship.

@martijnbastiaan martijnbastiaan enabled auto-merge (squash) June 5, 2025 13:39
@martijnbastiaan
Copy link
Member Author

Thank you :)

@martijnbastiaan martijnbastiaan merged commit 94bad16 into master Jun 5, 2025
14 checks passed
@martijnbastiaan martijnbastiaan deleted the martijn/add-clash-class-convert branch June 5, 2025 14:12
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants